/*
 * Original Author -> Harry Yang (taketoday@foxmail.com) https://taketoday.cn
 * Copyright © TODAY & 2017 - 2022 All Rights Reserved.
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see [http://www.gnu.org/licenses/]
 */

package cn.taketoday.expression.spel.ast;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;

import cn.taketoday.core.TypeDescriptor;
import cn.taketoday.core.bytecode.MethodVisitor;
import cn.taketoday.expression.AccessException;
import cn.taketoday.expression.EvaluationContext;
import cn.taketoday.expression.EvaluationException;
import cn.taketoday.expression.PropertyAccessor;
import cn.taketoday.expression.TypeConverter;
import cn.taketoday.expression.TypedValue;
import cn.taketoday.expression.spel.CodeFlow;
import cn.taketoday.expression.spel.ExpressionState;
import cn.taketoday.expression.spel.SpelEvaluationException;
import cn.taketoday.expression.spel.SpelMessage;
import cn.taketoday.expression.spel.support.ReflectivePropertyAccessor;
import cn.taketoday.lang.Assert;
import cn.taketoday.lang.Nullable;
import cn.taketoday.util.ReflectionUtils;

/**
 * An Indexer can index into some proceeding structure to access a particular piece of it.
 * Supported structures are: strings / collections (lists/sets) / arrays.
 *
 * @author Andy Clement
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @author Sam Brannen
 * @since 4.0
 */
// TODO support multidimensional arrays
// TODO support correct syntax for multidimensional [][][] and not [,,,]
public class Indexer extends SpelNodeImpl {

  private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT}

  // These fields are used when the indexer is being used as a property read accessor.
  // If the name and target type match these cached values then the cachedReadAccessor
  // is used to read the property. If they do not match, the correct accessor is
  // discovered and then cached for later use.

  @Nullable
  private String cachedReadName;

  @Nullable
  private Class<?> cachedReadTargetType;

  @Nullable
  private PropertyAccessor cachedReadAccessor;

  // These fields are used when the indexer is being used as a property write accessor.
  // If the name and target type match these cached values then the cachedWriteAccessor
  // is used to write the property. If they do not match, the correct accessor is
  // discovered and then cached for later use.

  @Nullable
  private String cachedWriteName;

  @Nullable
  private Class<?> cachedWriteTargetType;

  @Nullable
  private PropertyAccessor cachedWriteAccessor;

  @Nullable
  private IndexedType indexedType;

  public Indexer(int startPos, int endPos, SpelNodeImpl expr) {
    super(startPos, endPos, expr);
  }

  @Override
  public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
    return getValueRef(state).getValue();
  }

  @Override
  public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
    getValueRef(state).setValue(newValue);
  }

  @Override
  public boolean isWritable(ExpressionState expressionState) throws SpelEvaluationException {
    return true;
  }

  @Override
  protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
    TypedValue context = state.getActiveContextObject();
    Object target = context.getValue();
    TypeDescriptor targetDescriptor = context.getTypeDescriptor();
    TypedValue indexValue;
    Object index;

    // This first part of the if clause prevents a 'double dereference' of the property (SPR-5847)
    if (target instanceof Map && (this.children[0] instanceof PropertyOrFieldReference reference)) {
      index = reference.getName();
      indexValue = new TypedValue(index);
    }
    else {
      // In case the map key is unqualified, we want it evaluated against the root object
      // so temporarily push that on whilst evaluating the key
      try {
        state.pushActiveContextObject(state.getRootContextObject());
        indexValue = this.children[0].getValueInternal(state);
        index = indexValue.getValue();
        Assert.state(index != null, "No index");
      }
      finally {
        state.popActiveContextObject();
      }
    }

    // Raise a proper exception in case of a null target
    if (target == null) {
      throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE);
    }
    // At this point, we need a TypeDescriptor for a non-null target object
    Assert.state(targetDescriptor != null, "No type descriptor");

    // Indexing into a Map
    if (target instanceof Map<?, ?> map) {
      Object key = index;
      TypeDescriptor mapKeyDescriptor = targetDescriptor.getMapKeyDescriptor();
      if (mapKeyDescriptor != null) {
        key = state.convertValue(key, mapKeyDescriptor);
      }
      this.indexedType = IndexedType.MAP;
      return new MapIndexingValueRef(state.getTypeConverter(), map, key, targetDescriptor);
    }

    // If the object is something that looks indexable by an integer,
    // attempt to treat the index value as a number
    if (target.getClass().isArray() || target instanceof Collection || target instanceof String) {
      int idx = (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class));
      if (target.getClass().isArray()) {
        this.indexedType = IndexedType.ARRAY;
        return new ArrayIndexingValueRef(state.getTypeConverter(), target, idx, targetDescriptor);
      }
      else if (target instanceof Collection) {
        if (target instanceof List) {
          this.indexedType = IndexedType.LIST;
        }
        return new CollectionIndexingValueRef((Collection<?>) target, idx, targetDescriptor,
                state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
                state.getConfiguration().getMaximumAutoGrowSize());
      }
      else {
        this.indexedType = IndexedType.STRING;
        return new StringIndexingLValue((String) target, idx, targetDescriptor);
      }
    }

    // Try and treat the index value as a property of the context object
    // TODO: could call the conversion service to convert the value to a String
    TypeDescriptor valueType = indexValue.getTypeDescriptor();
    if (valueType != null && String.class == valueType.getType()) {
      this.indexedType = IndexedType.OBJECT;
      return new PropertyIndexingValueRef(
              target, (String) index, state.getEvaluationContext(), targetDescriptor);
    }

    throw new SpelEvaluationException(
            getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetDescriptor);
  }

  @Override
  public boolean isCompilable() {
    if (this.indexedType == IndexedType.ARRAY) {
      return (this.exitTypeDescriptor != null);
    }
    else if (this.indexedType == IndexedType.LIST) {
      return this.children[0].isCompilable();
    }
    else if (this.indexedType == IndexedType.MAP) {
      return (this.children[0] instanceof PropertyOrFieldReference || this.children[0].isCompilable());
    }
    else if (this.indexedType == IndexedType.OBJECT) {
      // If the string name is changing the accessor is clearly going to change (so no compilation possible)
      return (this.cachedReadAccessor != null &&
              this.cachedReadAccessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor &&
              getChild(0) instanceof StringLiteral);
    }
    return false;
  }

  @Override
  public void generateCode(MethodVisitor mv, CodeFlow cf) {
    String descriptor = cf.lastDescriptor();
    if (descriptor == null) {
      // Stack is empty, should use context object
      cf.loadTarget(mv);
    }

    if (this.indexedType == IndexedType.ARRAY) {
      int insn;
      if ("D".equals(this.exitTypeDescriptor)) {
        mv.visitTypeInsn(CHECKCAST, "[D");
        insn = DALOAD;
      }
      else if ("F".equals(this.exitTypeDescriptor)) {
        mv.visitTypeInsn(CHECKCAST, "[F");
        insn = FALOAD;
      }
      else if ("J".equals(this.exitTypeDescriptor)) {
        mv.visitTypeInsn(CHECKCAST, "[J");
        insn = LALOAD;
      }
      else if ("I".equals(this.exitTypeDescriptor)) {
        mv.visitTypeInsn(CHECKCAST, "[I");
        insn = IALOAD;
      }
      else if ("S".equals(this.exitTypeDescriptor)) {
        mv.visitTypeInsn(CHECKCAST, "[S");
        insn = SALOAD;
      }
      else if ("B".equals(this.exitTypeDescriptor)) {
        mv.visitTypeInsn(CHECKCAST, "[B");
        insn = BALOAD;
      }
      else if ("C".equals(this.exitTypeDescriptor)) {
        mv.visitTypeInsn(CHECKCAST, "[C");
        insn = CALOAD;
      }
      else {
        mv.visitTypeInsn(CHECKCAST, "[" + this.exitTypeDescriptor +
                (CodeFlow.isPrimitiveArray(this.exitTypeDescriptor) ? "" : ";"));
        //depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;");
        insn = AALOAD;
      }
      SpelNodeImpl index = this.children[0];
      cf.enterCompilationScope();
      index.generateCode(mv, cf);
      cf.exitCompilationScope();
      mv.visitInsn(insn);
    }

    else if (this.indexedType == IndexedType.LIST) {
      mv.visitTypeInsn(CHECKCAST, "java/util/List");
      cf.enterCompilationScope();
      this.children[0].generateCode(mv, cf);
      cf.exitCompilationScope();
      mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true);
    }

    else if (this.indexedType == IndexedType.MAP) {
      mv.visitTypeInsn(CHECKCAST, "java/util/Map");
      // Special case when the key is an unquoted string literal that will be parsed as
      // a property/field reference
      if ((this.children[0] instanceof PropertyOrFieldReference reference)) {
        String mapKeyName = reference.getName();
        mv.visitLdcInsn(mapKeyName);
      }
      else {
        cf.enterCompilationScope();
        this.children[0].generateCode(mv, cf);
        cf.exitCompilationScope();
      }
      mv.visitMethodInsn(
              INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true);
    }

    else if (this.indexedType == IndexedType.OBJECT) {
      ReflectivePropertyAccessor.OptimalPropertyAccessor accessor =
              (ReflectivePropertyAccessor.OptimalPropertyAccessor) this.cachedReadAccessor;
      Assert.state(accessor != null, "No cached read accessor");
      Member member = accessor.member;
      boolean isStatic = Modifier.isStatic(member.getModifiers());
      String classDesc = member.getDeclaringClass().getName().replace('.', '/');

      if (!isStatic) {
        if (descriptor == null) {
          cf.loadTarget(mv);
        }
        if (descriptor == null || !classDesc.equals(descriptor.substring(1))) {
          mv.visitTypeInsn(CHECKCAST, classDesc);
        }
      }

      if (member instanceof Method method) {
        mv.visitMethodInsn((isStatic ? INVOKESTATIC : INVOKEVIRTUAL), classDesc, member.getName(),
                CodeFlow.createSignatureDescriptor(method), false);
      }
      else {
        mv.visitFieldInsn((isStatic ? GETSTATIC : GETFIELD), classDesc, member.getName(),
                CodeFlow.toJvmDescriptor(((Field) member).getType()));
      }
    }

    cf.pushDescriptor(this.exitTypeDescriptor);
  }

  @Override
  public String toStringAST() {
    StringJoiner sj = new StringJoiner(",", "[", "]");
    for (SpelNodeImpl child : children) {
      sj.add(child.toStringAST());
    }
    return sj.toString();
  }

  private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue,
          Class<?> arrayComponentType) throws EvaluationException {

    if (arrayComponentType == Boolean.TYPE) {
      boolean[] array = (boolean[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, Boolean.class);
    }
    else if (arrayComponentType == Byte.TYPE) {
      byte[] array = (byte[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, Byte.class);
    }
    else if (arrayComponentType == Character.TYPE) {
      char[] array = (char[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, Character.class);
    }
    else if (arrayComponentType == Double.TYPE) {
      double[] array = (double[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, Double.class);
    }
    else if (arrayComponentType == Float.TYPE) {
      float[] array = (float[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, Float.class);
    }
    else if (arrayComponentType == Integer.TYPE) {
      int[] array = (int[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, Integer.class);
    }
    else if (arrayComponentType == Long.TYPE) {
      long[] array = (long[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, Long.class);
    }
    else if (arrayComponentType == Short.TYPE) {
      short[] array = (short[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, Short.class);
    }
    else {
      Object[] array = (Object[]) ctx;
      checkAccess(array.length, idx);
      array[idx] = convertValue(converter, newValue, arrayComponentType);
    }
  }

  private Object accessArrayElement(Object ctx, int idx) throws SpelEvaluationException {
    Class<?> arrayComponentType = ctx.getClass().getComponentType();
    if (arrayComponentType == Boolean.TYPE) {
      boolean[] array = (boolean[]) ctx;
      checkAccess(array.length, idx);
      this.exitTypeDescriptor = "Z";
      return array[idx];
    }
    else if (arrayComponentType == Byte.TYPE) {
      byte[] array = (byte[]) ctx;
      checkAccess(array.length, idx);
      this.exitTypeDescriptor = "B";
      return array[idx];
    }
    else if (arrayComponentType == Character.TYPE) {
      char[] array = (char[]) ctx;
      checkAccess(array.length, idx);
      this.exitTypeDescriptor = "C";
      return array[idx];
    }
    else if (arrayComponentType == Double.TYPE) {
      double[] array = (double[]) ctx;
      checkAccess(array.length, idx);
      this.exitTypeDescriptor = "D";
      return array[idx];
    }
    else if (arrayComponentType == Float.TYPE) {
      float[] array = (float[]) ctx;
      checkAccess(array.length, idx);
      this.exitTypeDescriptor = "F";
      return array[idx];
    }
    else if (arrayComponentType == Integer.TYPE) {
      int[] array = (int[]) ctx;
      checkAccess(array.length, idx);
      this.exitTypeDescriptor = "I";
      return array[idx];
    }
    else if (arrayComponentType == Long.TYPE) {
      long[] array = (long[]) ctx;
      checkAccess(array.length, idx);
      this.exitTypeDescriptor = "J";
      return array[idx];
    }
    else if (arrayComponentType == Short.TYPE) {
      short[] array = (short[]) ctx;
      checkAccess(array.length, idx);
      this.exitTypeDescriptor = "S";
      return array[idx];
    }
    else {
      Object[] array = (Object[]) ctx;
      checkAccess(array.length, idx);
      Object retValue = array[idx];
      this.exitTypeDescriptor = CodeFlow.toDescriptor(arrayComponentType);
      return retValue;
    }
  }

  private void checkAccess(int arrayLength, int index) throws SpelEvaluationException {
    if (index >= arrayLength) {
      throw new SpelEvaluationException(getStartPosition(), SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS,
              arrayLength, index);
    }
  }

  @SuppressWarnings("unchecked")
  private <T> T convertValue(TypeConverter converter, @Nullable Object value, Class<T> targetType) {
    T result = (T) converter.convertValue(
            value, TypeDescriptor.fromObject(value), TypeDescriptor.valueOf(targetType));
    if (result == null) {
      throw new IllegalStateException("Null conversion result for index [" + value + "]");
    }
    return result;
  }

  private class ArrayIndexingValueRef implements ValueRef {

    private final TypeConverter typeConverter;

    private final Object array;

    private final int index;

    private final TypeDescriptor typeDescriptor;

    ArrayIndexingValueRef(TypeConverter typeConverter, Object array, int index, TypeDescriptor typeDescriptor) {
      this.typeConverter = typeConverter;
      this.array = array;
      this.index = index;
      this.typeDescriptor = typeDescriptor;
    }

    @Override
    public TypedValue getValue() {
      Object arrayElement = accessArrayElement(this.array, this.index);
      return new TypedValue(arrayElement, this.typeDescriptor.elementDescriptor(arrayElement));
    }

    @Override
    public void setValue(@Nullable Object newValue) {
      TypeDescriptor elementType = this.typeDescriptor.getElementDescriptor();
      Assert.state(elementType != null, "No element type");
      setArrayElement(this.typeConverter, this.array, this.index, newValue, elementType.getType());
    }

    @Override
    public boolean isWritable() {
      return true;
    }
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  private class MapIndexingValueRef implements ValueRef {

    private final TypeConverter typeConverter;

    private final Map map;

    @Nullable
    private final Object key;

    private final TypeDescriptor mapEntryDescriptor;

    public MapIndexingValueRef(
            TypeConverter typeConverter, Map map, @Nullable Object key, TypeDescriptor mapEntryDescriptor) {

      this.typeConverter = typeConverter;
      this.map = map;
      this.key = key;
      this.mapEntryDescriptor = mapEntryDescriptor;
    }

    @Override
    public TypedValue getValue() {
      Object value = this.map.get(this.key);
      exitTypeDescriptor = CodeFlow.toDescriptor(Object.class);
      return new TypedValue(value, this.mapEntryDescriptor.getMapValueDescriptor(value));
    }

    @Override
    public void setValue(@Nullable Object newValue) {
      TypeDescriptor mapValueDescriptor = this.mapEntryDescriptor.getMapValueDescriptor();
      if (mapValueDescriptor != null) {
        newValue = typeConverter.convertValue(newValue, TypeDescriptor.fromObject(newValue), mapValueDescriptor);
      }
      this.map.put(this.key, newValue);
    }

    @Override
    public boolean isWritable() {
      return true;
    }
  }

  private class PropertyIndexingValueRef implements ValueRef {

    private final Object targetObject;

    private final String name;

    private final EvaluationContext evaluationContext;

    private final TypeDescriptor targetObjectTypeDescriptor;

    public PropertyIndexingValueRef(Object targetObject, String value,
            EvaluationContext evaluationContext, TypeDescriptor targetObjectTypeDescriptor) {

      this.targetObject = targetObject;
      this.name = value;
      this.evaluationContext = evaluationContext;
      this.targetObjectTypeDescriptor = targetObjectTypeDescriptor;
    }

    @Override
    public TypedValue getValue() {
      Class<?> targetObjectRuntimeClass = getObjectClass(this.targetObject);
      try {
        if (Indexer.this.cachedReadName != null && Indexer.this.cachedReadName.equals(this.name) &&
                Indexer.this.cachedReadTargetType != null &&
                Indexer.this.cachedReadTargetType.equals(targetObjectRuntimeClass)) {
          // It is OK to use the cached accessor
          PropertyAccessor accessor = Indexer.this.cachedReadAccessor;
          Assert.state(accessor != null, "No cached read accessor");
          return accessor.read(this.evaluationContext, this.targetObject, this.name);
        }
        List<PropertyAccessor> accessorsToTry = AstUtils.getPropertyAccessorsToTry(
                targetObjectRuntimeClass, this.evaluationContext.getPropertyAccessors());
        for (PropertyAccessor accessor : accessorsToTry) {
          if (accessor.canRead(this.evaluationContext, this.targetObject, this.name)) {
            if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) {
              accessor = reflectivePropertyAccessor.createOptimalAccessor(
                      this.evaluationContext, this.targetObject, this.name);
            }
            Indexer.this.cachedReadAccessor = accessor;
            Indexer.this.cachedReadName = this.name;
            Indexer.this.cachedReadTargetType = targetObjectRuntimeClass;
            if (accessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor optimalAccessor) {
              Member member = optimalAccessor.member;
              Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(member instanceof Method method ?
                                                                      method.getReturnType() : ((Field) member).getType());
            }
            return accessor.read(this.evaluationContext, this.targetObject, this.name);
          }
        }
      }
      catch (AccessException ex) {
        throw new SpelEvaluationException(getStartPosition(), ex,
                SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString());
      }
      throw new SpelEvaluationException(getStartPosition(),
              SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString());
    }

    @Override
    public void setValue(@Nullable Object newValue) {
      Class<?> contextObjectClass = getObjectClass(this.targetObject);
      try {
        if (Indexer.this.cachedWriteName != null && Indexer.this.cachedWriteName.equals(this.name) &&
                Indexer.this.cachedWriteTargetType != null &&
                Indexer.this.cachedWriteTargetType.equals(contextObjectClass)) {
          // It is OK to use the cached accessor
          PropertyAccessor accessor = Indexer.this.cachedWriteAccessor;
          Assert.state(accessor != null, "No cached write accessor");
          accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
          return;
        }
        List<PropertyAccessor> accessorsToTry = AstUtils.getPropertyAccessorsToTry(
                contextObjectClass, this.evaluationContext.getPropertyAccessors());
        for (PropertyAccessor accessor : accessorsToTry) {
          if (accessor.canWrite(this.evaluationContext, this.targetObject, this.name)) {
            Indexer.this.cachedWriteName = this.name;
            Indexer.this.cachedWriteTargetType = contextObjectClass;
            Indexer.this.cachedWriteAccessor = accessor;
            accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
            return;
          }
        }
      }
      catch (AccessException ex) {
        throw new SpelEvaluationException(getStartPosition(), ex,
                SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, this.name, ex.getMessage());
      }
    }

    @Override
    public boolean isWritable() {
      return true;
    }
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  private class CollectionIndexingValueRef implements ValueRef {

    private final Collection collection;

    private final int index;

    private final TypeDescriptor collectionEntryDescriptor;

    private final TypeConverter typeConverter;

    private final boolean growCollection;

    private final int maximumSize;

    public CollectionIndexingValueRef(Collection collection, int index, TypeDescriptor collectionEntryDescriptor,
            TypeConverter typeConverter, boolean growCollection, int maximumSize) {

      this.collection = collection;
      this.index = index;
      this.collectionEntryDescriptor = collectionEntryDescriptor;
      this.typeConverter = typeConverter;
      this.growCollection = growCollection;
      this.maximumSize = maximumSize;
    }

    @Override
    public TypedValue getValue() {
      growCollectionIfNecessary();
      if (this.collection instanceof List list) {
        Object o = list.get(this.index);
        exitTypeDescriptor = CodeFlow.toDescriptor(Object.class);
        return new TypedValue(o, this.collectionEntryDescriptor.elementDescriptor(o));
      }
      int pos = 0;
      for (Object o : this.collection) {
        if (pos == this.index) {
          return new TypedValue(o, this.collectionEntryDescriptor.elementDescriptor(o));
        }
        pos++;
      }
      throw new IllegalStateException("Failed to find indexed element " + this.index + ": " + this.collection);
    }

    @Override
    public void setValue(@Nullable Object newValue) {
      growCollectionIfNecessary();
      if (this.collection instanceof List list) {
        if (this.collectionEntryDescriptor.getElementDescriptor() != null) {
          newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.fromObject(newValue),
                  this.collectionEntryDescriptor.getElementDescriptor());
        }
        list.set(this.index, newValue);
      }
      else {
        throw new SpelEvaluationException(getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE,
                this.collectionEntryDescriptor.toString());
      }
    }

    private void growCollectionIfNecessary() {
      if (this.index >= this.collection.size()) {
        if (!this.growCollection) {
          throw new SpelEvaluationException(getStartPosition(), SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS,
                  this.collection.size(), this.index);
        }
        if (this.index >= this.maximumSize) {
          throw new SpelEvaluationException(getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION);
        }
        if (this.collectionEntryDescriptor.getElementDescriptor() == null) {
          throw new SpelEvaluationException(
                  getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE);
        }
        TypeDescriptor elementType = this.collectionEntryDescriptor.getElementDescriptor();
        try {
          Constructor<?> ctor = getDefaultConstructor(elementType.getType());
          int newElements = this.index - this.collection.size();
          while (newElements >= 0) {
            // Insert a null value if the element type does not have a default constructor.
            this.collection.add(ctor != null ? ctor.newInstance() : null);
            newElements--;
          }
        }
        catch (Throwable ex) {
          throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.UNABLE_TO_GROW_COLLECTION);
        }
      }
    }

    @Nullable
    private Constructor<?> getDefaultConstructor(Class<?> type) {
      try {
        return ReflectionUtils.accessibleConstructor(type);
      }
      catch (Throwable ex) {
        return null;
      }
    }

    @Override
    public boolean isWritable() {
      return true;
    }
  }

  private class StringIndexingLValue implements ValueRef {

    private final String target;

    private final int index;

    private final TypeDescriptor typeDescriptor;

    public StringIndexingLValue(String target, int index, TypeDescriptor typeDescriptor) {
      this.target = target;
      this.index = index;
      this.typeDescriptor = typeDescriptor;
    }

    @Override
    public TypedValue getValue() {
      if (this.index >= this.target.length()) {
        throw new SpelEvaluationException(getStartPosition(), SpelMessage.STRING_INDEX_OUT_OF_BOUNDS,
                this.target.length(), this.index);
      }
      return new TypedValue(String.valueOf(this.target.charAt(this.index)));
    }

    @Override
    public void setValue(@Nullable Object newValue) {
      throw new SpelEvaluationException(getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE,
              this.typeDescriptor.toString());
    }

    @Override
    public boolean isWritable() {
      return true;
    }
  }

}
